﻿using System;
using System.Collections.Generic;
using System.IO;
using System.Text;

namespace Apewer.Web
{

    /// <summary>静态站点控制器。</summary>
    public class StaticController : ApiController
    {

        /// <summary>允许服务器端包含（Server Side Include）。</summary>
        /// <remarks>默认值：允许。</remarks>
        protected bool AllowSSI { get; set; } = true;

        /// <summary>当执行目录且没有默认文档时，枚举子目录和子文件。</summary>
        /// <remarks>默认值：不允许。</remarks>
        protected bool AllowEnumerate { get; set; } = false;

        /// <summary>获取已解析的站点根目录。</summary>
        protected string Root { get => _root?.Value; }

        List<string> PathSegments;

        Class<string> _root = null;

        /// <summary></summary>
        public StaticController() : base((c) => { ((StaticController)c).Initialize(); return false; }) { }

        void Initialize()
        {
            if (Request == null || Request.Url == null) return;
            var absolute = Request.Url.AbsolutePath;
            var split = absolute == null ? new string[0] : absolute.Split('/', '\\');
            PathSegments = new List<string>(split.Length);
            foreach (var item in split)
            {
                var trim = TextUtility.Trim(TextUtility.DecodeUrl(item));
                if (string.IsNullOrEmpty(trim)) continue;
                if (trim == "." || trim == "..") continue;
                PathSegments.Add(trim);
            }
            absolute = TextUtility.Join("/", PathSegments);

            var path = MapPath(absolute);
            if (PathSegments.Count < 1) Directory(GetRoot());
            else if (IsBlocked(PathSegments[0])) Respond404(path);
            else if (StorageUtility.FileExists(path)) File(path);
            else if (StorageUtility.DirectoryExists(path)) Directory(path);
            else Respond404(path);
        }

        #region virtual

        /// <summary>响应 404 状态。</summary>
        /// <remarks>默认：设置状态为 404，不输出内容。</remarks>
        protected virtual void Respond404(string path)
        {
            Response.Model = new ApiStatusModel(404);
        }

        /// <summary>响应 403 状态。</summary>
        /// <remarks>默认：设置状态为 403，不输出内容。</remarks>
        protected virtual void Respond403(string path)
        {
            Response.Model = new ApiStatusModel(403);
        }

        /// <summary>获取此静态站点的目录。</summary>
        protected virtual string GetRoot()
        {
            if (_root) return _root.Value;
            var app = RuntimeUtility.ApplicationPath;

            var paths = StorageUtility.GetSubFiles(app);
            foreach (var path in paths)
            {
                var split = path.Split('/', '\\');
                var lower = split[split.Length - 1];
                switch (lower)
                {
                    case "index.html":
                    case "index.htm":
                    case "default.html":
                    case "default.htm":
                    case "favicon.ico":
                        _root = new Class<string>(app);
                        return app;
                }
            }

            var www = StorageUtility.CombinePath(app, "www");
            if (System.IO.Directory.Exists(www))
            {
                _root = new Class<string>(www);
                return www;
            }

            var web = StorageUtility.CombinePath(app, "web");
            if (System.IO.Directory.Exists(web))
            {
                _root = new Class<string>(web);
                return web;
            }

            var @static = StorageUtility.CombinePath(app, "static");
            if (System.IO.Directory.Exists(@static))
            {
                _root = new Class<string>(@static);
                return @static;
            }

            _root = new Class<string>(app);
            return app;
        }

        /// <summary>从扩展名获取内容类型。</summary>
        protected virtual string ContentType(string extension) => NetworkUtility.Mime(extension);

        /// <summary>从扩展名和文件路径获取过期时间。</summary>
        /// <remarks>默认值：0，不缓存。</remarks>
        protected virtual int Expires(string extension, string path) => 0;

        /// <summary>已解析到本地文本路径，执行此路径。</summary>
        /// <remarks>默认：输出文件内容，文件不存在时输出 404 状态。</remarks>
        protected virtual void File(string path)
        {
            if (!System.IO.File.Exists(path))
            {
                Respond404(path);
                return;
            }

            // 获取文件扩展名。
            var ext = Path.GetExtension(path).Lower();
            if (ext.Length > 1 && ext.StartsWith(".")) ext = ext.Substring(1);

            // 按扩展名获取缓存过期时间。
            var expires = Expires(ext, path);

            // 按扩展名获取 Content-Type。
            var type = ContentType(ext);
            if (string.IsNullOrEmpty(type)) type = NetworkUtility.Mime(ext);

            // Server Side Includes
            if (AllowSSI && ext == "html" || ext == "htm" || ext == "shtml")
            {
                var html = ReadWithSSI(path);
                var bytes = html.Bytes();

                var model = new ApiBytesModel();
                if (expires > 0) model.Expires = expires;
                model.ContentType = type;
                model.Bytes = bytes;

                Response.Model = model;
            }
            else
            {
                var stream = StorageUtility.OpenFile(path, true);

                var model = new ApiStreamModel();
                if (expires > 0) model.Expires = expires;
                model.ContentType = type;
                model.AutoDispose = true;
                model.Stream = stream;

                Response.Model = model;
            }
        }

        /// <summary>已解析到本地目录路径，执行此路径。</summary>
        /// <remarks>默认：输出文件内容，文件不存在时输出 404 状态。</remarks>
        protected virtual void Directory(string path)
        {
            if (!System.IO.Directory.Exists(path))
            {
                Respond404(path);
                return;
            }

            var @default = Default(path);
            if (!string.IsNullOrEmpty(@default))
            {
                File(@default);
                return;
            }

            if (AllowEnumerate) Response.Data = ListChildren(path);
            else Respond403(path);
        }

        /// <summary>在指定目录下搜索默认文件。</summary>
        /// <returns>完整文件路径，当搜索失败时返回 NULL。</returns>
        protected string Default(string directory)
        {
            var subs = StorageUtility.GetSubFiles(directory);
            if (subs.Count < 0) return null;
            subs.Sort();

            var names = new Dictionary<string, string>(subs.Count);
            foreach (var sub in subs)
            {
                var name = Path.GetFileName(sub);
                if (names.ContainsKey(name)) continue;
                names.Add(name, sub);
            }
            foreach (var sub in subs)
            {
                var name = Path.GetFileName(sub);
                var lower = name.ToLower();
                if (lower == name) continue;
                if (names.ContainsKey(lower)) continue;
                names.Add(lower, sub);
            }
            if (names.ContainsKey("index.html")) return names["index.html"];
            if (names.ContainsKey("index.htm")) return names["index.htm"];
            if (names.ContainsKey("default.html")) return names["default.html"];
            if (names.ContainsKey("default.htm")) return names["default.htm"];

            return null;
        }

        #endregion

        #region private

        // 解析 URL 的路径，获取本地路径。
        string MapPath(string urlPath)
        {
            var path = GetRoot();
            if (!string.IsNullOrEmpty(urlPath))
            {
                foreach (var split in urlPath.Split('/'))
                {
                    var seg = split.ToTrim();
                    if (string.IsNullOrEmpty(seg)) continue;
                    if (seg == "." || seg == "..") continue;
                    path = StorageUtility.CombinePath(path, seg);
                }
            }
            return path;
        }

        // Server Side Includes
        string ReadWithSSI(string path, int recursive = 0)
        {
            if (recursive > 10) return "";

            var input = StorageUtility.ReadFile(path, true);
            if (input == null || input.LongLength < 1) return "";

            // 尝试解码。
            var html = TextUtility.FromBytes(input);
            if (string.IsNullOrEmpty(html)) return "";

            // 按首尾截取。
            const string left = "<!--";
            const string right = "-->";
            const string head = "#include virtual=";
            var sb = new StringBuilder();
            var text = html;
            while (true)
            {
                var offset = text.IndexOf(left);
                if (offset < 0)
                {
                    sb.Append(text);
                    break;
                }
                if (offset > 0)
                {
                    sb.Append(text.Substring(0, offset));
                    text = text.Substring(offset + left.Length);
                }
                else text = text.Substring(left.Length);
                var length = text.IndexOf(right);
                if (length < 1)
                {
                    sb.Append(left);
                    sb.Append(text);
                    break;
                }
                var inner = text.Substring(0, length);
                var temp = inner.ToTrim();
                if (temp.StartsWith(head))
                {
                    temp = temp.Substring(head.Length);
                    temp = temp.Replace("\"", "");
                    var subPath = MapPath(temp);
                    var subText = ReadWithSSI(subPath, recursive + 1);
                    if (subText != null && subText.Length > 0) sb.Append(subText);
                }
                else
                {
                    sb.Append(left);
                    sb.Append(inner);
                    sb.Append(right);
                }
                text = text.Substring(length + right.Length);
            }

            var output = sb.ToString();
            return output;
        }

        /// <summary>列出指定目录的子项。</summary>
        Json ListChildren(string directory)
        {
            if (!System.IO.Directory.Exists(directory)) return null;
            var json = Json.NewObject();
            json.SetProperty("directories", ListDirectories(directory));
            json.SetProperty("files", ListFiles(directory));
            return json;
        }

        Json ListDirectories(string directory)
        {
            var array = Json.NewArray();
            var subs = StorageUtility.GetSubDirectories(directory);
            subs.Sort();
            foreach (var sub in subs)
            {
                var split = sub.Split('/', '\\');
                var name = split[split.Length - 1];
                if (IsBlocked(name)) continue;

                var json = Json.NewObject();
                json.SetProperty("name", name);
                try
                {
                    var info = new DirectoryInfo(sub);
                    json.SetProperty("modified", info.LastWriteTimeUtc.Stamp());
                }
                catch { }
                array.AddItem(json);
            }
            return array;
        }

        Json ListFiles(string directory)
        {
            var array = Json.NewArray();
            var subs = StorageUtility.GetSubFiles(directory);
            subs.Sort();
            foreach (var sub in subs)
            {
                var name = Path.GetFileName(sub);
                if (IsBlocked(name)) continue;

                var json = Json.NewObject();
                json.SetProperty("name", name);
                try
                {
                    var info = new FileInfo(sub);
                    json.SetProperty("size", info.Length);
                    json.SetProperty("modified", info.LastWriteTimeUtc.Stamp());
                }
                catch { }
                array.AddItem(json);
            }
            return array;
        }

        static bool IsBlocked(string segment)
        {
            if (string.IsNullOrEmpty(segment)) return true;
            var lower = segment.ToLower();
            switch (lower)
            {
                case ".":
                case "..":

                // Synology
                case "@eadir":
                case "#recycle":

                // Windows
                case "$recycle.bin":
                case "recycler":
                case "system volume information":
                case "desktop.ini":
                case "thumbs.db":

                // macOS
                case ".ds_store":
                case ".localized":

                // IIS
                case "app_code":
                case "app_data":
                case "aspnet_client":
                case "bin":
                case "web.config":

                    return true;
            }
            return false;
        }

        #endregion

    }

}
